با فرادادهنویسی در تایپاسکریپت از طریق بازتاب و تولید کد آشنا شوید. کد را در زمان کامپایل برای انتزاعهای قدرتمند تحلیل و دستکاری کنید.
فرادادهنویسی در تایپاسکریپت: بازتاب و تولید کد
فرادادهنویسی (Metaprogramming)، هنر نوشتن کدی که کدهای دیگر را دستکاری میکند، امکانات هیجانانگیزی را در تایپاسکریپت فراهم میآورد. این پست به حوزه فرادادهنویسی با استفاده از تکنیکهای بازتاب (reflection) و تولید کد (code generation) میپردازد و بررسی میکند که چگونه میتوانید کد خود را در حین کامپایل تحلیل و اصلاح کنید. ما ابزارهای قدرتمندی مانند دکوراتورها (decorators) و TypeScript Compiler API را بررسی خواهیم کرد تا شما را برای ساخت برنامههای قوی، قابل توسعه و با قابلیت نگهداری بالا توانمند سازیم.
فرادادهنویسی چیست؟
در هسته خود، فرادادهنویسی شامل نوشتن کدی است که بر روی کدهای دیگر عمل میکند. این به شما امکان میدهد تا به صورت پویا کد را در زمان کامپایل یا زمان اجرا تولید، تحلیل یا تبدیل کنید. در تایپاسکریپت، فرادادهنویسی عمدتاً بر روی عملیات زمان کامپایل متمرکز است و از سیستم نوع و خود کامپایلر برای دستیابی به انتزاعهای قدرتمند بهره میبرد.
در مقایسه با رویکردهای فرادادهنویسی زمان اجرا که در زبانهایی مانند پایتون یا روبی یافت میشود، رویکرد زمان کامپایل تایپاسکریپت مزایایی مانند موارد زیر را ارائه میدهد:
- ایمنی نوع (Type Safety): خطاها در حین کامپایل شناسایی میشوند و از رفتار غیرمنتظره در زمان اجرا جلوگیری میکنند.
- عملکرد: تولید و دستکاری کد قبل از زمان اجرا انجام میشود که منجر به اجرای بهینه کد میگردد.
- Intellisense و تکمیل خودکار: ساختارهای فرادادهنویسی میتوانند توسط سرویس زبان تایپاسکریپت درک شوند و پشتیبانی بهتری از ابزارهای توسعهدهنده فراهم کنند.
بازتاب در تایپاسکریپت
بازتاب، در زمینه فرادادهنویسی، توانایی یک برنامه برای بازرسی و اصلاح ساختار و رفتار خود است. در تایپاسکریپت، این موضوع عمدتاً شامل بررسی انواع، کلاسها، خصوصیات و متدها در زمان کامپایل است. در حالی که تایپاسکریپت یک سیستم بازتاب سنتی زمان اجرا مانند جاوا یا داتنت ندارد، ما میتوانیم از سیستم نوع و دکوراتورها برای دستیابی به اثرات مشابه استفاده کنیم.
دکوراتورها: حاشیهنویسی برای فرادادهنویسی
دکوراتورها یک ویژگی قدرتمند در تایپاسکریپت هستند که راهی برای افزودن حاشیهنویسی و اصلاح رفتار کلاسها، متدها، خصوصیات و پارامترها فراهم میکنند. آنها به عنوان ابزارهای فرادادهنویسی زمان کامپایل عمل میکنند و به شما امکان میدهند منطق سفارشی و فراداده را به کد خود تزریق کنید.
دکوراتورها با استفاده از نماد @ و به دنبال آن نام دکوراتور اعلام میشوند. آنها میتوانند برای موارد زیر استفاده شوند:
- افزودن فراداده به کلاسها یا اعضا.
- اصلاح تعاریف کلاس.
- پوشاندن یا جایگزین کردن متدها.
- ثبت کلاسها یا متدها در یک رجیستری مرکزی.
مثال: دکوراتور ثبت وقایع (Logging)
بیایید یک دکوراتور ساده ایجاد کنیم که فراخوانی متدها را ثبت میکند:
function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling method ${propertyKey} with arguments: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`Method ${propertyKey} returned: ${result}`);
return result;
};
return descriptor;
}
class MyClass {
@logMethod
add(x: number, y: number): number {
return x + y;
}
}
const myInstance = new MyClass();
myInstance.add(5, 3);
در این مثال، دکوراتور @logMethod فراخوانیهای متد add را رهگیری میکند، آرگومانها و مقدار بازگشتی را ثبت کرده و سپس متد اصلی را اجرا میکند. این نشان میدهد که چگونه میتوان از دکوراتورها برای افزودن دغدغههای فراگیر (cross-cutting concerns) مانند ثبت وقایع یا نظارت بر عملکرد بدون تغییر منطق اصلی کلاس استفاده کرد.
فکتوریهای دکوراتور
فکتوریهای دکوراتور به شما امکان میدهند دکوراتورهای پارامتری ایجاد کنید که آنها را انعطافپذیرتر و قابل استفاده مجدد میکند. فکتوری دکوراتور تابعی است که یک دکوراتور را برمیگرداند.
function logMethodWithPrefix(prefix: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`${prefix} - Calling method ${propertyKey} with arguments: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`${prefix} - Method ${propertyKey} returned: ${result}`);
return result;
};
return descriptor;
};
}
class MyClass {
@logMethodWithPrefix("DEBUG")
add(x: number, y: number): number {
return x + y;
}
}
const myInstance = new MyClass();
myInstance.add(5, 3);
در این مثال، logMethodWithPrefix یک فکتوری دکوراتور است که یک پیشوند به عنوان آرگومان میگیرد. دکوراتور بازگشتی، فراخوانیهای متد را با پیشوند مشخص شده ثبت میکند. این به شما امکان میدهد تا رفتار ثبت وقایع را بر اساس زمینه سفارشی کنید.
بازتاب فراداده با `reflect-metadata`
کتابخانه reflect-metadata یک راه استاندارد برای ذخیره و بازیابی فرادادههای مرتبط با کلاسها، متدها، خصوصیات و پارامترها فراهم میکند. این کتابخانه مکمل دکوراتورهاست و به شما امکان میدهد دادههای دلخواه را به کد خود متصل کرده و در زمان اجرا (یا زمان کامپایل از طریق تعاریف نوع) به آنها دسترسی پیدا کنید.
برای استفاده از reflect-metadata، باید آن را نصب کنید:
npm install reflect-metadata --save
و گزینه کامپایلر emitDecoratorMetadata را در فایل tsconfig.json خود فعال کنید:
{
"compilerOptions": {
"emitDecoratorMetadata": true
}
}
مثال: اعتبارسنجی خصوصیت
بیایید یک دکوراتور ایجاد کنیم که مقادیر خصوصیات را بر اساس فراداده اعتبارسنجی میکند:
import 'reflect-metadata';
const requiredMetadataKey = Symbol("required");
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor) {
let method = descriptor.value!;
descriptor.value = function () {
let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (arguments.length <= parameterIndex || arguments[parameterIndex] === undefined) {
throw new Error("Missing required argument.");
}
}
}
return method.apply(this, arguments);
};
}
class MyClass {
myMethod(@required param1: string, param2: number) {
console.log(param1, param2);
}
}
در این مثال، دکوراتور @required پارامترها را به عنوان الزامی علامتگذاری میکند. دکوراتور validate فراخوانیهای متد را رهگیری کرده و بررسی میکند که آیا تمام پارامترهای الزامی وجود دارند یا خیر. اگر یک پارامتر الزامی وجود نداشته باشد، یک خطا پرتاب میشود. این نشان میدهد که چگونه میتوان از reflect-metadata برای اجرای قوانین اعتبارسنجی بر اساس فراداده استفاده کرد.
تولید کد با TypeScript Compiler API
TypeScript Compiler API دسترسی برنامهنویسی به کامپایلر تایپاسکریپت را فراهم میکند و به شما امکان میدهد کد تایپاسکریپت را تحلیل، تبدیل و تولید کنید. این امر امکانات قدرتمندی را برای فرادادهنویسی باز میکند و شما را قادر میسازد تا تولیدکنندگان کد سفارشی، لینترها و سایر ابزارهای توسعه را بسازید.
درک درخت نحو انتزاعی (AST)
پایه و اساس تولید کد با Compiler API، درخت نحو انتزاعی (AST) است. AST یک نمایش درختی از کد تایپاسکریپت شماست که در آن هر گره در درخت، یک عنصر نحوی مانند کلاس، تابع، متغیر یا عبارت را نشان میدهد.
Compiler API توابعی را برای پیمایش و دستکاری AST فراهم میکند که به شما امکان میدهد ساختار کد خود را تحلیل و اصلاح کنید. شما میتوانید از AST برای موارد زیر استفاده کنید:
- استخراج اطلاعات در مورد کد شما (مثلاً، پیدا کردن تمام کلاسهایی که یک رابط خاص را پیادهسازی میکنند).
- تبدیل کد شما (مثلاً، تولید خودکار کامنتهای مستندات).
- تولید کد جدید (مثلاً، ایجاد کد تکراری برای اشیاء دسترسی به داده).
مراحل تولید کد
جریان کاری معمول برای تولید کد با Compiler API شامل مراحل زیر است:
- تجزیه (Parse) کد تایپاسکریپت: از تابع
ts.createSourceFileبرای ایجاد یک شیء SourceFile استفاده کنید که کد تجزیه شده تایپاسکریپت را نشان میدهد. - پیمایش AST: از توابع
ts.visitNodeوts.visitEachChildبرای پیمایش بازگشتی AST و یافتن گرههای مورد نظر خود استفاده کنید. - تبدیل AST: گرههای AST جدید ایجاد کنید یا گرههای موجود را برای پیادهسازی تحولات مورد نظر خود اصلاح کنید.
- تولید کد تایپاسکریپت: از تابع
ts.createPrinterبرای تولید کد تایپاسکریپت از AST اصلاح شده استفاده کنید.
مثال: تولید یک شیء انتقال داده (DTO)
بیایید یک تولیدکننده کد ساده ایجاد کنیم که یک رابط شیء انتقال داده (DTO) را بر اساس تعریف یک کلاس تولید میکند.
import * as ts from "typescript";
import * as fs from "fs";
function generateDTO(sourceFile: ts.SourceFile, className: string): string | undefined {
let interfaceName = className + "DTO";
let properties: string[] = [];
function visit(node: ts.Node) {
if (ts.isClassDeclaration(node) && node.name?.text === className) {
node.members.forEach(member => {
if (ts.isPropertyDeclaration(member) && member.name) {
let propertyName = member.name.getText(sourceFile);
let typeName = "any"; // Default type
if (member.type) {
typeName = member.type.getText(sourceFile);
}
properties.push(` ${propertyName}: ${typeName};`);
}
});
}
}
ts.visitNode(sourceFile, visit);
if (properties.length > 0) {
return `interface ${interfaceName} {\n${properties.join("\n")}\n}`;
}
return undefined;
}
// Example Usage
const fileName = "./src/my_class.ts"; // Replace with your file path
const classNameToGenerateDTO = "MyClass";
fs.readFile(fileName, (err, buffer) => {
if (err) {
console.error("Error reading file:", err);
return;
}
const sourceCode = buffer.toString();
const sourceFile = ts.createSourceFile(
fileName,
sourceCode,
ts.ScriptTarget.ES2015,
true
);
const dtoInterface = generateDTO(sourceFile, classNameToGenerateDTO);
if (dtoInterface) {
console.log(dtoInterface);
} else {
console.log(`Class ${classNameToGenerateDTO} not found or no properties to generate DTO from.`);
}
});
my_class.ts:
class MyClass {
name: string;
age: number;
isActive: boolean;
}
این مثال یک فایل تایپاسکریپت را میخواند، کلاسی با نام مشخص را پیدا میکند، خصوصیات و انواع آنها را استخراج کرده و یک رابط DTO با همان خصوصیات تولید میکند. خروجی به این صورت خواهد بود:
interface MyClassDTO {
name: string;
age: number;
isActive: boolean;
}
توضیح:
- کد منبع فایل تایپاسکریپت را با استفاده از
fs.readFileمیخواند. - یک
ts.SourceFileاز کد منبع با استفاده ازts.createSourceFileایجاد میکند که کد تجزیه شده را نشان میدهد. - تابع
generateDTOاز AST بازدید میکند. اگر یک تعریف کلاس با نام مشخص پیدا شود، در میان اعضای کلاس پیمایش میکند. - برای هر تعریف خصوصیت، نام و نوع خصوصیت را استخراج کرده و آن را به آرایه
propertiesاضافه میکند. - در نهایت، رشته رابط DTO را با استفاده از خصوصیات استخراج شده میسازد و آن را برمیگرداند.
کاربردهای عملی تولید کد
تولید کد با Compiler API کاربردهای عملی متعددی دارد، از جمله:
- تولید کد تکراری: تولید خودکار کد برای اشیاء دسترسی به داده، کلاینتهای API یا سایر وظایف تکراری.
- ایجاد لینترهای سفارشی: اجرای استانداردهای کدنویسی و بهترین شیوهها با تحلیل AST و شناسایی مشکلات بالقوه.
- تولید مستندات: استخراج اطلاعات از AST برای تولید مستندات API.
- خودکارسازی بازآرایی (Refactoring): بازآرایی خودکار کد با تبدیل AST.
- ساخت زبانهای خاص دامنه (DSLs): ایجاد زبانهای سفارشی متناسب با دامنههای خاص و تولید کد تایپاسکریپت از آنها.
تکنیکهای پیشرفته فرادادهنویسی
فراتر از دکوراتورها و Compiler API، چندین تکنیک دیگر نیز میتوانند برای فرادادهنویسی در تایپاسکریپت استفاده شوند:
- انواع شرطی (Conditional Types): استفاده از انواع شرطی برای تعریف انواع بر اساس انواع دیگر، که به شما امکان میدهد تعاریف نوع انعطافپذیر و سازگار ایجاد کنید. برای مثال، میتوانید نوعی ایجاد کنید که نوع بازگشتی یک تابع را استخراج کند.
- انواع نگاشتشده (Mapped Types): تبدیل انواع موجود با نگاشت بر روی خصوصیات آنها، که به شما امکان میدهد انواع جدیدی با انواع یا نامهای خصوصیت اصلاح شده ایجاد کنید. برای مثال، ایجاد نوعی که تمام خصوصیات یک نوع دیگر را فقط-خواندنی (read-only) میکند.
- استنتاج نوع (Type Inference): بهرهگیری از قابلیتهای استنتاج نوع تایپاسکریپت برای استنتاج خودکار انواع بر اساس کد، که نیاز به حاشیهنویسی صریح نوع را کاهش میدهد.
- انواع رشتهای الگو (Template Literal Types): استفاده از انواع رشتهای الگو برای ایجاد انواع مبتنی بر رشته که میتوانند برای تولید کد یا اعتبارسنجی استفاده شوند. برای مثال، تولید کلیدهای خاص بر اساس ثابتهای دیگر.
مزایای فرادادهنویسی
فرادادهنویسی مزایای متعددی را در توسعه تایپاسکریپت ارائه میدهد:
- افزایش قابلیت استفاده مجدد کد: ایجاد مؤلفهها و انتزاعهای قابل استفاده مجدد که میتوانند در بخشهای مختلف برنامه شما به کار روند.
- کاهش کد تکراری: تولید خودکار کدهای تکراری، که میزان کدنویسی دستی مورد نیاز را کاهش میدهد.
- بهبود قابلیت نگهداری کد: با جداسازی دغدغهها و استفاده از فرادادهنویسی برای مدیریت دغدغههای فراگیر، کد خود را ماژولارتر و قابل فهمتر کنید.
- افزایش ایمنی نوع: شناسایی خطاها در حین کامپایل و جلوگیری از رفتار غیرمنتظره در زمان اجرا.
- افزایش بهرهوری: خودکارسازی وظایف و سادهسازی جریانهای کاری توسعه، که منجر به افزایش بهرهوری میشود.
چالشهای فرادادهنویسی
در حالی که فرادادهنویسی مزایای قابل توجهی دارد، چالشهایی را نیز به همراه دارد:
- افزایش پیچیدگی: فرادادهنویسی میتواند کد شما را پیچیدهتر و درک آن را دشوارتر کند، به خصوص برای توسعهدهندگانی که با تکنیکهای مربوطه آشنا نیستند.
- مشکلات اشکالزدایی (Debugging): اشکالزدایی کد فرادادهنویسی میتواند چالشبرانگیزتر از کد سنتی باشد، زیرا کدی که اجرا میشود ممکن است مستقیماً در کد منبع قابل مشاهده نباشد.
- سربار عملکرد: تولید و دستکاری کد میتواند سربار عملکردی ایجاد کند، به خصوص اگر با دقت انجام نشود.
- منحنی یادگیری: تسلط بر تکنیکهای فرادادهنویسی نیازمند سرمایهگذاری قابل توجهی از زمان و تلاش است.
نتیجهگیری
فرادادهنویسی در تایپاسکریپت، از طریق بازتاب و تولید کد، ابزارهای قدرتمندی برای ساخت برنامههای قوی، قابل توسعه و با قابلیت نگهداری بالا ارائه میدهد. با بهرهگیری از دکوراتورها، TypeScript Compiler API و ویژگیهای پیشرفته سیستم نوع، میتوانید وظایف را خودکار کرده، کد تکراری را کاهش دهید و کیفیت کلی کد خود را بهبود بخشید. در حالی که فرادادهنویسی چالشهایی را به همراه دارد، مزایایی که ارائه میدهد آن را به یک تکنیک ارزشمند برای توسعهدهندگان باتجربه تایپاسکریپت تبدیل میکند.
قدرت فرادادهنویسی را در آغوش بگیرید و امکانات جدیدی را در پروژههای تایپاسکریپت خود باز کنید. مثالهای ارائه شده را کاوش کنید، با تکنیکهای مختلف آزمایش کنید و کشف کنید که چگونه فرادادهنویسی میتواند به شما در ساخت نرمافزار بهتر کمک کند.